package pl.edu.agh.pros.xmpp;

import java.util.HashSet;
import java.util.Set;

import org.apache.commons.lang.RandomStringUtils;
import org.jivesoftware.smack.Connection;
import org.jivesoftware.smack.ConnectionConfiguration;
import org.jivesoftware.smack.PacketListener;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smackx.Form;
import org.jivesoftware.smackx.FormField;
import org.jivesoftware.smackx.muc.MultiUserChat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import pl.edu.agh.pros.conf.XMPPConfiguration;
import pl.edu.agh.pros.internal.interf.Disposable;

public class XMPPManager implements Disposable {

	private static final int CREDENTIAL_LENGTH = 19;
	private static final int RETRY_COUNT = 5;
	private static Logger logger = LoggerFactory.getLogger(XMPPManager.class);
	private volatile Connection connection;
	private String conferenceService;
	private String user;
	private String password;
	private ConnectionConfiguration config;
	private Set<PacketListener> packetListeners = new HashSet<PacketListener>();

	private XMPPManager(XMPPConfiguration xmppConfiguration) throws XMPPException {
		this.connectAndLogin(xmppConfiguration);
		this.conferenceService = xmppConfiguration.getXmppRoomServiceName();
		Runtime.getRuntime().addShutdownHook(new Thread() {
			@Override
			public void run() {
				disconnect();
			}
		});
	}

	public static synchronized XMPPManager newInstance(XMPPConfiguration xmppConfiguration) throws XMPPException {
		return new XMPPManager(xmppConfiguration);
	}

	public String getLoggedUser() {
		return connection.getUser();
	}

	public void sendMessage(Message message, String receiver) {
		try {
			message.setTo(receiver);
			connection.sendPacket(message);
		} catch (Exception e) {
			logger.debug("Can not send packet.", e); //$NON-NLS-1$
		}
	}

	public void addPacketListener(PacketListener packetListener) {
		packetListeners.add(packetListener);
		connection.addPacketListener(packetListener, null);
	}

	public MultiUserChat createMultiUserChat(String roomName, String id) throws XMPPException {
		MultiUserChat muc = new MultiUserChat(connection, getFullRoomName(roomName));
		muc.create(id);
		Form form = muc.getConfigurationForm();
		Form answer = form.createAnswerForm();
		FormField fieldMaxUsers = new FormField("muc#roomconfig_maxusers"); //$NON-NLS-1$
		fieldMaxUsers.addValue("0");// 0 means unlimited //$NON-NLS-1$
		answer.addField(fieldMaxUsers);
		muc.sendConfigurationForm(answer);
		return muc;
	}

	public MultiUserChat joinMultiUserChat(String roomName, String id) throws XMPPException {
		MultiUserChat muc = new MultiUserChat(connection, getFullRoomName(roomName));
		muc.join(id);
		return muc;
	}

	public boolean isConnected() {
		return connection != null && connection.isConnected();
	}

	public void reconnect() {
		try {
			if (connection == null || !connection.isConnected()) {
				connection = new XMPPConnection(config);
				connection.connect();
			}
			if (connection != null && !connection.isAuthenticated()) {
				login(user, password);
				for (PacketListener packetListener : packetListeners) {
					connection.addPacketListener(packetListener, null);
				}
			}
		} catch (XMPPException e) {
			logger.debug("Can not reconnect.", e); //$NON-NLS-1$
		}
	}

	public void disconnect() {
		try {
			if (connection.isConnected()) {
				connection.getAccountManager().deleteAccount();
			}
			logger.info("Removed temporary account."); //$NON-NLS-1$
		} catch (XMPPException e) {
			logger.error("Can not delete account ", e); //$NON-NLS-1$
		}
		connection.disconnect();
		connection = null;
	}

	private Connection connectAndLogin(XMPPConfiguration xmppConfiguration) throws XMPPException {
		return connectAndLogin(xmppConfiguration.getXmppServerAddress(), xmppConfiguration.getXmppServerPort(),
				xmppConfiguration.getXmppUsername(), xmppConfiguration.getXmppPassword());
	}

	private Connection connectAndLogin(String server, int port, String user, String password) throws XMPPException {
		connect(server, port);
		login(user, password);
		return connection;
	}

	private void connect(String server, int port) throws XMPPException {
		config = new ConnectionConfiguration(server, port);
		if (connection == null || !connection.isConnected()) {
			connection = new XMPPConnection(config);
			connection.connect();
		}
	}

	private void login(String user, String password) throws XMPPException {
		if (user == null || password == null) {
			logger.info("User or password not specified for XMPP connection, logging annonymously."); //$NON-NLS-1$
			try {
				connection.loginAnonymously();
			} catch (XMPPException e) {
				logger.info("Annonymous login not supported on this server. Trying to create temporary account."); //$NON-NLS-1$
				boolean userLogged = createAccountAndLoginWithRetry();
				if (userLogged) {
					logger.info("User successfully logged."); //$NON-NLS-1$
				} else {
					logger.error("Can not create account."); //$NON-NLS-1$
					throw new XMPPException();
				}
			}
		} else {
			connection.login(user, password, String.valueOf(System.currentTimeMillis()));
			this.user = user;
			this.password = password;
		}

	}

	private boolean createAccountAndLoginWithRetry() {
		for (int i = 0; i < RETRY_COUNT; i++) {
			try {
				String user = RandomStringUtils.randomAlphanumeric(CREDENTIAL_LENGTH);
				String password = RandomStringUtils.randomAlphanumeric(CREDENTIAL_LENGTH);
				connection.getAccountManager().createAccount(user, password);
				connection.login(user, password);
				this.user = user;
				this.password = password;
				return true;
			} catch (Exception e) {
				logger.info("Failed to create account, retrying"); //$NON-NLS-1$
			}
		}
		return false;
	}

	private String getFullRoomName(String roomName) {
		return roomName + "@" + conferenceService; //$NON-NLS-1$
	}

	@Override
	public void dispose() {
		// TODO Auto-generated method stub

	}

}
